Add ADB device tracking (host:track-devices) for real-time device monitoring#327
Add ADB device tracking (host:track-devices) for real-time device monitoring#327
Conversation
Add AdbDeviceTracker class for real-time device connection monitoring via the ADB daemon socket protocol. Connects to localhost:5037, sends host:track-devices-l, and pushes device list updates through a callback. Features: - Auto-reconnect with exponential backoff (500ms to 16s) - Callback-based StartAsync() with CancellationToken support - CurrentDevices snapshot property - IDisposable lifecycle management - Reuses AdbRunner.ParseAdbDevicesOutput() for parsing Includes 11 unit tests covering protocol parsing, edge cases, and lifecycle management. PublicAPI entries for both net10.0 and netstandard2.0. Closes #323 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new AdbDeviceTracker public API to Xamarin.Android.Tools.AndroidSdk to monitor ADB device connect/disconnect events in real time using the host:track-devices-l daemon socket protocol.
Changes:
- Introduces
AdbDeviceTrackerwith reconnect/backoff, snapshot state (CurrentDevices), and callback-driven tracking viaStartAsync. - Adds unit tests validating lifecycle guards and length-prefixed protocol parsing behavior.
- Updates PublicAPI unshipped files for
netstandard2.0andnet10.0.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbDeviceTrackerTests.cs | Adds tests for tracker lifecycle and length-prefixed message parsing. |
| src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbDeviceTracker.cs | Implements TCP-based host:track-devices-l tracking loop with reconnect/backoff and parsing. |
| src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt | Registers the new public API surface for netstandard2.0. |
| src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt | Registers the new public API surface for net10.0. |
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.IO; | ||
| using System.Linq; |
There was a problem hiding this comment.
using System.Linq; is not used in this file and will trigger CS8019 (and can fail Release builds when warnings are treated as errors). Remove the unnecessary using.
| using System.Linq; |
| readonly int port; | ||
| readonly Action<TraceLevel, string> logger; | ||
| readonly string? adbPath; | ||
| readonly IDictionary<string, string>? environmentVariables; | ||
| IReadOnlyList<AdbDeviceInfo> currentDevices = Array.Empty<AdbDeviceInfo> (); | ||
| CancellationTokenSource? trackingCts; | ||
| bool disposed; | ||
|
|
||
| /// <summary> | ||
| /// Creates a new AdbDeviceTracker. | ||
| /// </summary> | ||
| /// <param name="adbPath">Optional path to the adb executable for starting the server if needed.</param> | ||
| /// <param name="port">ADB daemon port (default 5037).</param> | ||
| /// <param name="environmentVariables">Optional environment variables for adb processes.</param> | ||
| /// <param name="logger">Optional logger callback.</param> | ||
| public AdbDeviceTracker (string? adbPath = null, int port = 5037, | ||
| IDictionary<string, string>? environmentVariables = null, | ||
| Action<TraceLevel, string>? logger = null) | ||
| { | ||
| if (port <= 0 || port > 65535) | ||
| throw new ArgumentOutOfRangeException (nameof (port), "Port must be between 1 and 65535."); | ||
| this.adbPath = adbPath; | ||
| this.port = port; | ||
| this.environmentVariables = environmentVariables; | ||
| this.logger = logger ?? RunnerDefaults.NullLogger; |
There was a problem hiding this comment.
adbPath and environmentVariables are assigned but never used, which will raise CS0414 (and can fail Release builds when warnings are treated as errors). Either implement the advertised behavior (e.g., start/ensure the ADB server using adbPath and pass environmentVariables to that process) or remove these fields/ctor parameters to avoid a misleading public API surface.
| trackingCts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken); | ||
| var token = trackingCts.Token; | ||
| var backoffMs = InitialBackoffMs; | ||
|
|
||
| while (!token.IsCancellationRequested) { | ||
| try { | ||
| await TrackDevicesAsync (onDevicesChanged, token).ConfigureAwait (false); | ||
| } catch (OperationCanceledException) when (token.IsCancellationRequested) { | ||
| break; | ||
| } catch (Exception ex) { | ||
| logger.Invoke (TraceLevel.Warning, $"ADB tracking connection lost: {ex.Message}. Reconnecting in {backoffMs}ms..."); | ||
| try { | ||
| await Task.Delay (backoffMs, token).ConfigureAwait (false); | ||
| } catch (OperationCanceledException) { | ||
| break; | ||
| } | ||
| backoffMs = Math.Min (backoffMs * 2, MaxBackoffMs); | ||
| continue; | ||
| } | ||
| // Reset backoff on clean connection | ||
| backoffMs = InitialBackoffMs; |
There was a problem hiding this comment.
StartAsync creates and stores a linked CancellationTokenSource but never disposes it when StartAsync completes (only on Dispose()), and repeated StartAsync calls will overwrite trackingCts. Consider using a local CTS with a try/finally to dispose it, and guarding against multiple concurrent starts (throw or no-op) to avoid leaks/races.
| trackingCts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken); | |
| var token = trackingCts.Token; | |
| var backoffMs = InitialBackoffMs; | |
| while (!token.IsCancellationRequested) { | |
| try { | |
| await TrackDevicesAsync (onDevicesChanged, token).ConfigureAwait (false); | |
| } catch (OperationCanceledException) when (token.IsCancellationRequested) { | |
| break; | |
| } catch (Exception ex) { | |
| logger.Invoke (TraceLevel.Warning, $"ADB tracking connection lost: {ex.Message}. Reconnecting in {backoffMs}ms..."); | |
| try { | |
| await Task.Delay (backoffMs, token).ConfigureAwait (false); | |
| } catch (OperationCanceledException) { | |
| break; | |
| } | |
| backoffMs = Math.Min (backoffMs * 2, MaxBackoffMs); | |
| continue; | |
| } | |
| // Reset backoff on clean connection | |
| backoffMs = InitialBackoffMs; | |
| var linkedCts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken); | |
| if (Interlocked.CompareExchange (ref trackingCts, linkedCts, null) != null) { | |
| linkedCts.Dispose (); | |
| throw new InvalidOperationException ("Device tracking has already been started."); | |
| } | |
| try { | |
| var token = linkedCts.Token; | |
| var backoffMs = InitialBackoffMs; | |
| while (!token.IsCancellationRequested) { | |
| try { | |
| await TrackDevicesAsync (onDevicesChanged, token).ConfigureAwait (false); | |
| } catch (OperationCanceledException) when (token.IsCancellationRequested) { | |
| break; | |
| } catch (Exception ex) { | |
| logger.Invoke (TraceLevel.Warning, $"ADB tracking connection lost: {ex.Message}. Reconnecting in {backoffMs}ms..."); | |
| try { | |
| await Task.Delay (backoffMs, token).ConfigureAwait (false); | |
| } catch (OperationCanceledException) { | |
| break; | |
| } | |
| backoffMs = Math.Min (backoffMs * 2, MaxBackoffMs); | |
| continue; | |
| } | |
| // Reset backoff on clean connection | |
| backoffMs = InitialBackoffMs; | |
| } | |
| } finally { | |
| Interlocked.CompareExchange (ref trackingCts, null, linkedCts); | |
| linkedCts.Dispose (); |
Summary
Add support for real-time device connection/disconnection monitoring via the ADB daemon socket protocol (\host:track-devices-l).
Changes
et10.0\ and
etstandard2.0\
API
\\csharp
public sealed class AdbDeviceTracker : IDisposable
{
public AdbDeviceTracker(string? adbPath = null, int port = 5037, ...);
public IReadOnlyList CurrentDevices { get; }
public Task StartAsync(Action<IReadOnlyList> onDevicesChanged, CancellationToken cancellationToken = default);
public void Dispose();
}
\\
Closes #323